#include "OgreGLESHardwareBufferManager.h"
#include "OgreGLESHardwareVertexBuffer.h"
#include "OgreException.h"
#include "OgreLogManager.h"

namespace Ogre
{
	GLESHardwareVertexBuffer::GLESHardwareVertexBuffer(HardwareBufferManagerBase* mgr,
														size_t vertexSize,
														size_t numVertices,
														HardwareBuffer::Usage usage,
														bool useShadowBuffer)
		: HardwareVertexBuffer(mgr, vertexSize, numVertices, usage, false, useShadowBuffer)
	{
		if (!useShadowBuffer)
		{
			OGRE_EXCEPT(Exception::ERR_INTERNAL_ERROR,
						"Only support with shadowBuffer",
						"GLESHardwareVertexBuffer");
		}

		glGenBuffers(1, &mBufferId);

		if (!mBufferId)
		{
			OGRE_EXCEPT(Exception::ERR_INTERNAL_ERROR,
				"Cannot create GL vertex buffer",
				"GLESHardwareVertexBuffer::GLESHardwareVertexBuffer");
		}

		clearData();
	}

	GLESHardwareVertexBuffer::~GLESHardwareVertexBuffer()
	{
		glDeleteBuffers(1, &mBufferId);
	}

	void* GLESHardwareVertexBuffer::lockImpl(size_t offset,
											size_t length,
											LockOptions options)
	{
		if (mIsLocked)
		{
			OGRE_EXCEPT(Exception::ERR_INTERNAL_ERROR,
						"Invalid attempt to lock an index buffer that has already been locked",
						"GLESHardwareVertexBuffer::lock");
		}

		void* retPtr = 0;

		if (length < OGRE_GL_MAP_BUFFER_THRESHOLD)
		{
			retPtr = static_cast<GLESHardwareBufferManager*>(
				HardwareBufferManager::getSingletonPtr())->allocateScratch((uint32)length);

			if (retPtr)
			{
				mLockedToScratch = true;
				mScratchOffset = offset;
				mScratchSize = length;
                mScratchPtr = retPtr;
				mScratchUploadOnUnlock = (options != HBL_READ_ONLY);

				if (options != HBL_DISCARD)
				{
					readData(offset, length, retPtr);
				}
			}
		}
		else
		{
			OGRE_EXCEPT(Exception::ERR_INTERNAL_ERROR,
                        "Invalid Buffer lockSize",
                        "GLESHardwareVertexBuffer::lock");
		}
		return retPtr;
	}

	void GLESHardwareVertexBuffer::unlockImpl(void)
	{
		if (mLockedToScratch)
		{
			if (mScratchUploadOnUnlock)
			{
				// have to write the data back to vertex buffer
				writeData(mScratchOffset, mScratchSize, mScratchPtr,
                              mScratchOffset == 0 && mScratchSize == getSizeInBytes());
			}

			static_cast<GLESHardwareBufferManager*>(
				HardwareBufferManager::getSingletonPtr())->deallocateScratch(mScratchPtr);

			mLockedToScratch = false;
		}
		else
		{
			OGRE_EXCEPT(Exception::ERR_INTERNAL_ERROR,
				"Only locking to scratch is supported",
				"GLESHardwareVertexBuffer::unlockImpl");
		}
	}

	void GLESHardwareVertexBuffer::readData(size_t offset, size_t length, 
		void* pDest)
	{
		if (mUseShadowBuffer)
		{
			void* srcData = mpShadowBuffer->lock(offset, length, HBL_READ_ONLY);
			memcpy(pDest, srcData, length);
			mpShadowBuffer->unlock();
		}
		else
		{
			OGRE_EXCEPT(Exception::ERR_INTERNAL_ERROR,
						"Read hardware buffer is not supported",
						"GLESHardwareVertexBuffer::readData");
		}
	}

	void GLESHardwareVertexBuffer::writeData(size_t offset,
											size_t length,
											const void* pSource,
											bool discardWholeBuffer)
	{
		glBindBuffer(GL_ARRAY_BUFFER, mBufferId);

		// Update the shadow buffer
		if (mUseShadowBuffer)
		{
			void* pDestData = mpShadowBuffer->lock(offset, length,
											discardWholeBuffer ? HBL_DISCARD : HBL_NORMAL);

			memcpy(destData, pSource, length);
			mpShadowBuffer->unlock();
		}

		if (offset == 0 && length == mSizeInBytes)
		{
			glBufferData(GL_ARRAY_BUFFER, mSizeInBytes, pSource,
							GLESHardwareBufferManager::getGLUsage(mUsage));
		}
		else
		{
			if (discardWholeBuffer)
			{
				clearData();
			}

			glBufferSubData(GL_ARRAY_BUFFER, offset, length, pSource);
		}
	}

	void GLESHardwareVertexBuffer::_updateFromShadow(void)
	{
		if (mUseShadowBuffer && mShadowUpdated && !mSuppressHardwareUpdate)
		{
			const void *srcData = mpShadowBuffer->lock(mLockStart,
													   mLockSize,
													   HBL_READ_ONLY);

			glBindBuffer(GL_ARRAY_BUFFER, mBufferId);

			// Update whole buffer if possible, otherwise normal
            if (mLockStart == 0 && mLockSize == mSizeInBytes)
            {
                glBufferData(GL_ARRAY_BUFFER, mSizeInBytes, srcData,
                             GLESHardwareBufferManager::getGLUsage(mUsage));
            }
            else
            {
                glBufferSubData(GL_ARRAY_BUFFER, mLockStart, mLockSize, srcData);
            }

			mpShadowBuffer->unlock();
			mShadowUpdated = false;
		}
	}

	void GLESHardwareVertexBuffer::clearData(void)
	{
		void *ptr;

		ptr = OGRE_MALLOC(mSizeInBytes + 1, MEMCATEGORY_GEOMETRY);
		memset(ptr, 0, mSizeInBytes);

		glBindBuffer(GL_ARRAY_BUFFER, mBufferId);
		glBufferData(GL_ARRAY_BUFFER, mSizeInBytes, ptr,
					 GLESHardwareBufferManager::getGLUsage(mUsage));

		free(ptr);
	}
}
